Skip to content

Conversation

ampaze
Copy link
Contributor

@ampaze ampaze commented Sep 18, 2025

I hope this PR can spark a discussion on how to implement VoP.

Using this code I have successfully send SEPA Transfers with VoP, I tested Full Match, Partial Match and No match.

I would consider this code a Proof of Concept, the next step would be to integrate it in the FinTs class and not inside the action.

Usage with the current version is

$transferAction = SendSEPATransferVoP::create($sepaAccount, $payload);

$fints->execute($transferAction);
$this->checkTanNeedForAction($transferAction, $fints);

// VoP handling

while ($transferAction->needsTime()) {
    $wait = $transferAction->hivpp->wartezeitVorNaechsterAbfrage;
    $output->writeln("Warte auf Bereitstellung der VoP Prüfungsergebnisse, warte $wait Sekunden");
    sleep($wait);

    $fints->execute($transferAction);
}

if ($transferAction->needsConfirmation()) {
    // Check the result from the bank via $transferAction->hivpp
    $transferAction->setConfirmed();
    $fints->execute($transferAction);
}

$this->checkTanNeedForAction($transferAction, $fints);

$transferAction->ensureDone();

refs #477

Copy link
Contributor

@Philipp91 Philipp91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

It seems like the HIVPP and HKVPA part is still missing. Is that because you simply made some progress so far and wanted to put it out there, or because the pieces that are in this PR so far already work end-to-end (i.e. we wouldn't need these other two segments to make banks accept transfers at least)?


$hkvpp = HKVPPv1::createEmpty();

# For now just pretend we support all formats
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean if we pretend that we support all formats? Does the bank somehow get to choose one of them and then we have to be able to do something with it? Or is it not the library but instead the application (caller of phpFinTS) that would need to understand this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't gotten that far, as I have no way to test this. My guess is that the bank will chose one format from that list.

@ampaze
Copy link
Contributor Author

ampaze commented Sep 19, 2025

Our banks don't have any support for VoP yet, so i started implementing this without being able to test it. Maybe someone with a VoP ready bank can use it to help development. Also I wanted to get feedback early on, as I am still in the process of understanding VoP and how phpFinTS should implement it.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 9, 2025

@Philipp91 Ich hab jetzt ein Problem mit deinem Parser

/** @var string[] @Max(N) Max length each: 6 */
public array $vopPflichtigerZahlungsverkehrsauftrag;

In der Spezifikation steht bei 'VOP-pflichtiger Zahlungsverkehrsauftrag' Anzahl 'n', wie lässt sich das abbilden?
Wenn ich @Max weglasse, bekomme ich den Fehler:

"message": "Repeated property Property [ public array $vopPflichtigerZahlungsverkehrsauftrag ]\n needs @Max() annotation",

@witschko
Copy link

witschko commented Oct 9, 2025

@Philipp91 Ich hab jetzt ein Problem mit deinem Parser

/** @var string[] @Max(N) Max length each: 6 */
public array $vopPflichtigerZahlungsverkehrsauftrag;

In der Spezifikation steht bei 'VOP-pflichtiger Zahlungsverkehrsauftrag' Anzahl 'n', wie lässt sich das abbilden? Wenn ich @Max weglasse, bekomme ich den Fehler:

"message": "Repeated property Property [ public array $vopPflichtigerZahlungsverkehrsauftrag ]\n needs @Max() annotation",

Das ging mir genauso. Ich habe den Code nach @Max(N) durchsucht und kein Vorkommen gefunden. Ich habe zumindest temporär das N durch 99 ersetzt. Ob das korrekt ist, weiß ich nicht. Möglicherweise muss eher der Parser angepasst werden.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 9, 2025

Es gibt ein neuen Zwischenstand, aber Ich hab jetzt folgendes Problem:

Bei der Prüfung auf einen erwarteten Rückmeldungscode

if ($response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null) {

wird der Code (3945) nicht gefunden obwohl er in der Antwort der Bank enthalten ist:

HNHBK:1:3+000000000684+300+CM5100912235476+2+CM5100912235476:2'HNVSK:998:3+PIN:2+998+1+2::i2dmM9BkyJkBAAA0iju/lmuowAQA+1:20251009:122355+2:2:13:@8@00000000:5:1+280:20690500:PRIVATE_______:V:0:0+0'HNVSD:999:1+@458@HNSHK:2:4+PIN:2+946+8769273+1+1+2::i2dmM9BkyJkBAAA0iju/lmuowAQA+1+1:20251009:122355+1:999:1+6:10:19+280:20690500:PRIVATE_______:S:0:0'HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.+3905::Es wurde keine Challenge erzeugt.'HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref'HIRMS:5:2:5+3945::Freigabe ohne VOP-Best?tigung nicht m?glich.'HIVPP:6:1:3+++@36@667b82cb-8dab-467b-8374-6b19d2563873+++++2'HNSHA:7:2+8769273''HNHBS:8:1+2'


Edit:
Es liegt daran, dass Fints::processActionResponse die Rückmeldungen zum HKTAN Segment rausfiltert. Spricht was dagegen, die TAN Rückmeldungen nicht rauszufiltern?

@Philipp91
Copy link
Contributor

Laut FinTS_3.0_Formals_2017-10-06_final_version.pdf PDF-Seite 19 bedeutet Status=M Anzahl=n effektiv "einmal oder mehr".

Es steht nicht explizit in der Spezifikation, aber es ergibt sich aus dem serialisierten Datenformat (das der Parser lesen können muss), dass es nach einem solchen "einmal oder mehr" Feld keine weiteren Felder geben darf. Weil sonst wüsste man nicht, welche Felder noch zu dem "einmal oder mehr" gehören und welches Feld dann das darauffolgende ist. Denn in der serialisierten Form sind die Felder nicht (wie bei JSON) benannt oder (wie bei Protocol Buffers) nummeriert. Sondern die Werte für die Felder tauchen einfach einer nach dem anderen auf und man muss (anhand der Spezifikation) wissen, welches welches ist.

Die bisherige Implementierung des Parsers ist in der Hinsicht defensiv, d.h. solche "einmal oder mehr" Felder werden gar nicht erlaubt, weil sie bislang offenbar schlicht nicht vorkamen. Das kann aber geändert werden. Ich denke, wir sollten eine neue Annotation @Unlimited oder so einführen, damit man nicht aus Versehen @Max(..) vergisst. Und ich glaube, man könnte dann $maxCount = PHP_INT_MAX; setzen (wobei ich schon noch alle Auswirkungen davon genauer durchschauen würde). Und dann am Anfang der Schleife wenn $nextIndex > PHP_INT_MAX dann einen Fehler werfen, weil das @Unlimited Feld nicht das letzte war.

@Philipp91
Copy link
Contributor

Es liegt daran, dass Fints::processActionResponse die Rückmeldungen zum HKTAN Segment rausfiltert. Spricht was dagegen, die TAN Rückmeldungen nicht rauszufiltern?

Es scheint nicht die processActionResponse() Funktion selbst zu sein, sondern der Aufruf davon:

$this->processActionResponse($action, $response->filterByReferenceSegments($action->getRequestSegmentNumbers()));

Und es wird nicht explizit HKTAN weggefiltert, sondern es werden nur diejenigen Rückmeldungen an die Action weitergeleitet, die sich auf Segmente beziehen, die von der Action an den Server geschickt wurden. Das macht eigentlich schon Sinn, oder?


Blöde Frage: Ist Freigabe ohne VOP-Best?tigung nicht m?glich. ein Fehler (also etwas, was den Prozess insgesamt fehlschlagen lässt) oder einfach der Trigger dafür, dass die VoP Prüfung durchgeführt werden muss?


Wenn ich das richtig sehe, hast du im aktuellen PR mit SendSEPATransferVoP einen Fork von SendSEPATransfer erstellt, der auch VoP unterstützt. Damit ist die VoP-Implementierung spezifisch für SendSEPATransferVoP. Spiegelt das die Idee von VoP in der Spezifikation wider (also ist VoP nur für SEPA-Transfers gedacht) oder ist es eher eine übergreifende Funktion?

Selbst SendSEPATransfer an sich ist ja nicht ein einzelner Geschäftsvorfall, sondern verwendet intern CME, CSE, CCM und CCS. Ich gehe davon aus, dass VoP mindestens alle davon betrifft. Die Frage ist, ob es in Zukunft auch noch Anwendungen von VoP geben könnte, die nicht unter SendSEPATransfer fallen würden. Die VoP-Spezifikation nennt die Namen CME, CSE, CCM und CCS fast gar nicht, nur an zwei Stellen jeweils einen davon als Beispiel. Also scheint die Spezifikation allgemeiner zu sein.

Ein Alternatives Design wäre, VoP nicht als Subklasse zu implementieren, sondern SendSEPATransfer an sich weiter zu verwenden und stattdessen in der FinTs-Klasse an sich das ganze VoP-Handling zu integrieren. Das kann natürlich neue Helfer-Funktionen wie BaseAction::supportsVoP() oder BaseAction::processVoPThingy() nach sich ziehen, falls Action-spezifische Entscheidungen getroffen werden müssen (z.B. das Feld "VOP-pflichtiger Zahlungsverkehrsauftrag" ausfüllen), würde aber das Handling an sich beim zentralen Framwork belassen. Dort hätte man dann Zugriff auf alle Rückmeldungen und auch sonst noch weitergehende Möglichkeiten.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 10, 2025

Die bisherige Implementierung des Parsers ist in der Hinsicht defensiv, d.h. solche "einmal oder mehr" Felder werden gar nicht erlaubt, weil sie bislang offenbar schlicht nicht vorkamen. Das kann aber geändert werden. Ich denke, wir sollten eine neue Annotation @Unlimited oder so einführen, damit man nicht aus Versehen @Max(..) vergisst. Und ich glaube, man könnte dann $maxCount = PHP_INT_MAX; setzen (wobei ich schon noch alle Auswirkungen davon genauer durchschauen würde). Und dann am Anfang der Schleife wenn $nextIndex > PHP_INT_MAX dann einen Fehler werfen, weil das @Unlimited Feld nicht das letzte war.

Danke für die Erläuterung. Klingt soweit gut. Ich habe jetzt vorläufig @Max(999999) eingetragen. Das könnte gern später und separat von diesem PR geändert werden.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 10, 2025

Blöde Frage: Ist Freigabe ohne VOP-Best?tigung nicht m?glich. ein Fehler (also etwas, was den Prozess insgesamt fehlschlagen lässt) oder einfach der Trigger dafür, dass die VoP Prüfung durchgeführt werden muss?

Ja diese Meldung ist kein "Fehler" sondern ein Hinweis, dass man die HKVPA Bestätigung senden soll.

Selbst SendSEPATransfer an sich ist ja nicht ein einzelner Geschäftsvorfall, sondern verwendet intern CME, CSE, CCM und CCS. Ich gehe davon aus, dass VoP mindestens alle davon betrifft. Die Frage ist, ob es in Zukunft auch noch Anwendungen von VoP geben könnte, die nicht unter SendSEPATransfer fallen würden. Die VoP-Spezifikation nennt die Namen CME, CSE, CCM und CCS fast gar nicht, nur an zwei Stellen jeweils einen davon als Beispiel. Also scheint die Spezifikation allgemeiner zu sein.

HIVPPS sagt akuell,

vopPflichtigerZahlungsverkehrsauftrag] => Array
(
    [0] => HKCCS
    [1] => HKCDE
    [2] => HKCDN
    [3] => HKCSE
    [4] => HKCSA
    [5] => HKIPZ
    [6] => HKCCM
    [7] => HKCME
    [8] => HKZDF
    [9] => HKZDA
)

Das spricht eher für die Behandlung direkt von der Basisklasse. Ich würde es jetzt erstmal in die Richtung mit der Action weitermachen. Dann kann es die Grundlage für Weiteres sein.

Ich will es zumindest einmal eine Überweisung mit VoP durchgeführt haben um den ganzen Prozess zu verstehen.

@Philipp91
Copy link
Contributor

Als temporären Workaround kannst du natürlich den filterByReferenceSegments() Aufruf rausnehmen. Oder, wenn das andere Actions durcheinander bringt, ihn nur dann überspringen, wenn $action instanceof SendSEPATransferVoP.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 10, 2025

Mit dem aktuellen Versionsstand funktioniert es. Für mich sind jetzt noch offene Fragen, inwieweit FinTs das Prüfergebnis parsen und betrachten soll. Insbesondere bei Sammelüberweisungen muss das eigentlich die Anwendung machen, also prüfen welche Überweisung welchen Prüfstatus hat und die Wahl dem Endnutzer lassen.

Da man auf FinTs Ebene letztlich nur sagt, "(trotzdem) ausführen", kann ja die Anwendung entscheiden ob das getan werden soll oder nicht.

Wer also dringende Überweisungen mit FinTs zu tätigen hat, kann diesen PR gerne testen und Feedback geben.


Die Funktionalität in die FinTs Klasse zu überführen kann ich nächste Woche angehen. Alternativ kann das auch jemand anders machen 😏

@Philipp91
Copy link
Contributor

HIVPPS sagt akuell, vopPflichtigerZahlungsverkehrsauftrag] => Array ...

Ah das ist ja interessant. Das heißt, mithilfe der BPD kann die zentrale FinTs-Klasse einfach anhand der Request-Segmente, die die Action produziert hat, erkennen, dass VoP nötig ist. Das muss die Action gar nicht selber wissen/deklarieren. BaseAction::supportsVoP() kann man also gleich wieder streichen.

Die Funktionalität in die FinTs Klasse zu überführen kann ich nächste Woche angehen. Alternativ kann das auch jemand anders machen 😏

Ich kann es mir gerne mal anschauen. Dazu muss ich natürlich erst verstehen, wie die bisherige Implementierung hier funktioniert. Ich stelle dann mal ein paar Fragen.

Copy link
Contributor

@Philipp91 Philipp91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe den Code mal grob durchgelesen und versucht zu verstehen, wie die VoP-Sequenz funktioniert. Dabei habe ich ein paar Kommentare produziert, die nicht unbedingt alle beantwortet werden müssen (teilweise einfach kleine Details um den Code schöner zu machen, oder dieselben Fragen in verschiedenen Varianten).

Es wäre super, wenn du mir durch gezielte Beantwortung der aus deiner Sicht relevantesten Fragen helfen könntest, den Vorgang besser zu verstehen.


Und du hast ja geschrieben, dass es schon funktioniert. Das einzige was noch fehlt, wäre, dass man das Prüfergebnis irgendwie verarbeitet und Richtung Nutzer schickt, und dass der Code ein bisschen verschönert und idealerweise in die FinTs-Klasse gezogen wird. Das bedeutet aber auch, dass die exakten Daten, die über die Leitung gehen, sich nicht mehr ändern werden. Im Gegenteil: Wenn wir jetzt anhang von einer (anonymisierten) "Aufnahme" von Requests/Responses, die du von einer erfolgreichen Überweisung machst, einen Unit-Test bauen könnten, der das Ganze automatisiert wieder abspielen kann ohne echte Bank, dann könnte man viel mutiger Refactorings angehen, weil der Unit-Test ja deren Korrektheit beweisen würde.

if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) {
$message->add(HKTANFactory::createProzessvariante2Step1(
$this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment));
$hktan = HKTANFactory::createProzessvariante2Step1($this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diese Code-Änderung hat keine Auswirkung, oder? Könnte man also auch rückgängig machen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doch, ich brauche die Variable um die Segmentnummer zu ermitteln, damit es nicht rausgefiltert wird. Da ist aber nur relevant wenn die Action selbst die VoP sachen macht.

return;
}

if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::PAGINATION)) !== null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das ist code 3040. In der VoP-Spezifikation kommt der nicht so explizit vor. Ergibt sich das daraus, dass dort von "Aufsetzpunktmechanismus" die Rede ist? Oder hast du die 3040 einfach in der freien Wildbahn beobachtet?

Der Begriff "Pagination" passt dann nicht mehr so richtig. Wenn das wirklich identisch ist mit dem Aufsetzpunkt der für Pagination verwendet wird, sollten wir es in "continuation (token)" oder so umbenennen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe lange nicht verstanden wo der Aufsetzpunkt herkommen soll, der in der Spezifikation erwähnt wird. Bis ich dann draufgekommen bin das es Aufsetzpunkte ja schon öfter gab. Und ja: ohne den Aufsetzpunkt funktioniert das Polling der Ergebnisse nicht.

public ?HKVPPv1 $hkvpp = null;
public ?HIVPPv1 $hivpp = null;

protected function createRequest(BPD $bpd, ?UPD $upd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Für mein besseres Verständnis: Diese Funktion wird ja nun vermutlich einige Male nacheinander aufgerufen. Anhand des Zustands der obigen Member-Variablen muss die Action dann entscheiden können, welche Requests jeweils gesendet werden sollen.

Könntest du mir bitte einen Überblick geben, was der Reihe nach passiert? Vielleicht in Form einer Chronologie, welche von den ifs unten nacheinander zutreffen? Oder in Form eines Logs von Requests und Responses? Oder als Beschreibung der Zustands-Änderungen (also wann geht vopNeedsConfirmation auf true, wann auf false, wann relativ dazu geht vopIsPending auf true und so weiter)? Ich weiß nicht, in welcher Form es sich am besten erklären lässt. (Im Idealfall kann man eine einfachst-mögliche Erklärung am Ende auch in einfachen Code gießen.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Ablauf ist in der Spezifikation bereits als Diagram dargestellt. Grob gesagt:

  • Prüfauftrag (HKVPP) + Geschäftsvorfall abschicken
  • Bank antwortet mit, keine Prüfautrag nötig oder mit HIVPP (der dann entweder das Prüfergebnis + VoP-Id enthält oder eine Polling-Id und keine VoP-Id).
  • Abhängig davon muss ggf. solange HKVPP (Polling-Id + Aufsetzpunkt) nochmal schicken abfragen bis man eine VoP-Id ()bekommen hat.
  • Wenn man endlich eine VoP-Id hat, dann kann man den Ausführungsauftrag (HKVPA) + den ursprünglichen Geschäftsvorfall abschicken.
  • Dann kommt die normale Tan-Behandlung

$requestSegment = parent::createRequest($bpd, $upd);
$requestSegments = [$requestSegment];

if ($this->vopNeedsConfirmation && $this->vopConfirmed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hier fehlt mir ein erklärender Kommentar.

return $this->vopIsPending;
}

public function needsConfirmation()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsConfirmation() ist, wenn der Endnutzer etwas tun muss (z.b. 2FA bestätigen auf dem Handy). Das scheint hier nicht der Fall zu sein, oder? (Sonst würde ich eine neue Methode FinTs::confirmVoP() erwarten analog zu submitTan().)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah jetzt sehe ich deinen Beispielcode. Also braucht es doch beides und statt FinTs::confirmVoP() haben wir im Moment Action::setConfirmed() (was für mich wie ein dummer Setter klang, aber was wohl eine Funktion mit mehr Tragweite ist).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsConfirmation ist wenn die Bank einen HKVPA verlangt. Das ist also außer in bestimmten Sonderfällen eigentlich immer der Fall.
Dann muss der Nutzer ja irgendwie mitteilen, dass er diese Bestätigung auch erteilt. Das hatte ich setConfirmed genannt. Beides muss True sein bevor man einen Ausführungsauftrag sendet. FinTs::confirmVoP() ist aber in der Tat viel klarer.

@Philipp91
Copy link
Contributor

In deinem Beispielcode wird checkTanNeedForAction() zwei Mal aufgerufen. Einmal beim ursprünglichen Einreichen des Auftrags und einmal beim Bestätigen am Schluss. Sind beide notwendig?

@Philipp91
Copy link
Contributor

Für mich sind jetzt noch offene Fragen, inwieweit FinTs das Prüfergebnis parsen und betrachten soll.

Ah, also bevor setConfirmed(); aufgerufen wird, oder?

Welche Informationen stellt uns die Bank denn zur Verfügung, die man dem Nutzer hier anzeigen könnte?

@ampaze
Copy link
Contributor Author

ampaze commented Oct 11, 2025

In deinem Beispielcode wird checkTanNeedForAction() zwei Mal aufgerufen. Einmal beim ursprünglichen Einreichen des Auftrags und einmal beim Bestätigen am Schluss. Sind beide notwendig?

Gute Frage, da die Bank (theoretisch) den Auftrag auch gleich annehmen kann ohne HKVPA, kann an der Stelle schon die Tan Challenge kommen. Es wäre in dem Falle aber needsTime und needsConfirmation false, sodass man sowieso direkt zum zweiten Tan Check käme.

Für mich sind jetzt noch offene Fragen, inwieweit FinTs das Prüfergebnis parsen und betrachten soll.

Ah, also bevor setConfirmed(); aufgerufen wird, oder?

Exakt.

Welche Informationen stellt uns die Bank denn zur Verfügung, die man dem Nutzer hier anzeigen könnte?

Die Bank liefert einen pain.002.001.10 XML Customer Report, dort ist für jede einzelne Überweisung das Ergebnis der Prüfung drin und auch ein Gesamtwert. Dann gibt es noch HIVPP::aufklaerungstextAutorisierungTrotzAbweichung mit rechtlichem Bla Bla, der wohl dem Nutzer angezeigt werden muss/soll/kann.

Und du hast ja geschrieben, dass es schon funktioniert. Das einzige was noch fehlt, wäre, dass man das Prüfergebnis irgendwie verarbeitet und Richtung Nutzer schickt, und dass der Code ein bisschen verschönert und idealerweise in die FinTs-Klasse gezogen wird. Das bedeutet aber auch, dass die exakten Daten, die über die Leitung gehen, sich nicht mehr ändern werden. Im Gegenteil: Wenn wir jetzt anhang von einer (anonymisierten) "Aufnahme" von Requests/Responses, die du von einer erfolgreichen Überweisung machst, einen Unit-Test bauen könnten, der das Ganze automatisiert wieder abspielen kann ohne echte Bank, dann könnte man viel mutiger Refactorings angehen, weil der Unit-Test ja deren Korrektheit beweisen würde.

Ich kann dir entweder die Logs schicken oder es nächste Woche anvisieren den gewünschten Test zu schreiben.


Ich kann auch anbieten nächste Woche einen neuen PR zu machen, der nur die neuen Segmente und Rückmeldungscodes enthält, aber keine weitere Logik.

@Philipp91
Copy link
Contributor

Für @Unlimited ist die Implementierung hier. Noch nicht getestet, in der Annahme, dass dein Test-Case das dann automatisch auch abdeckt.

Ich kann dir entweder die Logs schicken oder es nächste Woche anvisieren den gewünschten Test zu schreiben.

Cool! Wie du möchtest.

Ich kann auch anbieten nächste Woche einen neuen PR zu machen, der nur die neuen Segmente und Rückmeldungscodes enthält, aber keine weitere Logik.

Auch das wäre nützlich, dann könnte man mit Mergen anfangen. Aber den hier unbedingt bestehen lassen, denn die weitere Logik wird ja schon benötigt (nur in anderer Form) und hier kann man sie schon anschauen.


Ich denke, so kommen wir gut vorwärts.

Nur falls es irgendwo hakt, könnte ich auch von der anderen Seite anfangen (habe ich heute überlegt, bin dann aber nicht dazu gekommen): Wenn du möchtest, könnte ich in FinTs ein paar neue API-Funktionen mit Dokumentation schreiben, sowie dein Beispiel aus dem Post oben in Samples integrieren, und dann könntest du die nötige Logik in die Funktionen reintun und gleich testen. Aber dein Vorschlag funktioniert genauso und ein PR nur mit Segmenten und Rückmeldungscodes ist sowieso eine super Idee, dann ist der PR insgesamt nicht so groß und das ist ja doch ein recht mechanischer Teil.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 13, 2025

Ich hab jetzt einen Test geschrieben, der zweimal VoP durchläuft. Einmal mit Prüfergebnis "No Match" und einmal mit "Match".

@Philipp91 Kannnst/möchtest du mit diesem Test die Implementation machen?

@witschko
Copy link

Es gibt ein neuen Zwischenstand, aber Ich hab jetzt folgendes Problem:

Bei der Prüfung auf einen erwarteten Rückmeldungscode

if ($response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null) {

wird der Code (3945) nicht gefunden obwohl er in der Antwort der Bank enthalten ist:

HNHBK:1:3+000000000684+300+CM5100912235476+2+CM5100912235476:2'HNVSK:998:3+PIN:2+998+1+2::i2dmM9BkyJkBAAA0iju/lmuowAQA+1:20251009:122355+2:2:13:@8@00000000:5:1+280:20690500:PRIVATE_______:V:0:0+0'HNVSD:999:1+@458@HNSHK:2:4+PIN:2+946+8769273+1+1+2::i2dmM9BkyJkBAAA0iju/lmuowAQA+1+1:20251009:122355+1:999:1+6:10:19+280:20690500:PRIVATE_______:S:0:0'HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.+3905::Es wurde keine Challenge erzeugt.'HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref'HIRMS:5:2:5+3945::Freigabe ohne VOP-Best?tigung nicht m?glich.'HIVPP:6:1:3+++@36@667b82cb-8dab-467b-8374-6b19d2563873+++++2'HNSHA:7:2+8769273''HNHBS:8:1+2'

Edit: Es liegt daran, dass Fints::processActionResponse die Rückmeldungen zum HKTAN Segment rausfiltert. Spricht was dagegen, die TAN Rückmeldungen nicht rauszufiltern?

Ich habe gerade den aktuellen Stand getestet und stoße auf dieses Problem, ist das nach wie vor so oder kann ich irgendetwas tun?

@ampaze
Copy link
Contributor Author

ampaze commented Oct 13, 2025

Ich habe gerade den aktuellen Stand getestet und stoße auf dieses Problem, ist das nach wie vor so oder kann ich irgendetwas tun?

Der Code funktioniert bei mir. Für das konkrete Problem hatte ich einen Workaround eingebaut. Hast du wirklich den letzten Stand?

Copy link
Contributor

@Philipp91 Philipp91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vielen Dank für den Test!

Ich gehe davon aus, dass er funktioniert, also dass composer test keine Fehler wirft.

@Philipp91
Copy link
Contributor

Die Bank liefert einen pain.002.001.10 XML Customer Report

Kann diese Bibliothek dieses Format heute schon parsen? Oder wäre es ziemlich einfach, das zu implementieren? Oder ist es eher etwas, was man wie SEPA-XML an eine andere Bibliothek outsourcen würde?

@witschko
Copy link

Ich habe gerade den aktuellen Stand getestet und stoße auf dieses Problem, ist das nach wie vor so oder kann ich irgendetwas tun?

Der Code funktioniert bei mir. Für das konkrete Problem hatte ich einen Workaround eingebaut. Hast du wirklich den letzten Stand?

Ich habe folgenden Stand: commit 60a62ee (HEAD -> verification-of-payee, origin/verification-of-payee)

Und wenn ich es richtig interpretiere, ist der Code in der Rückmeldung enthalten:

HNHBK:1:3+000000000692+300+1E5101409145483+3+1E5101409145483:3'HNVSK:998:3+PIN:2+998+1+2::r9UFoCSF4ZkBAACu3fYKh7BCCgQA+1:20251014:091455+2:2:13:@8@00000000:5:1+280:43060967:PRIVATE___________:V:0:0+0'HNVSD:999:1+@462@HNSHK:2:4+PIN:2+946+5174841+1+1+2::r9UFoCSF4ZkBAACu3fYKh7BCCgQA+1+1:20251014:091455+1:999:1+6:10:19+280:43060967:PRIVATE___________:S:0:0'HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.+3905::Es wurde keine Challenge erzeugt.'HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref'HIRMS:5:2:5+3945::Freigabe ohne VOP-Bestätigung nicht möglich.'HIVPP:6:1:3+++@36@8508d3e5-c111-4f12-9c29-ff5f29a16e8c+++++2'HNSHA:7:2+5174841''HNHBS:8:1+3'

@Philipp91
Copy link
Contributor

Kannnst/möchtest du mit diesem Test die Implementation machen?

Am Mittwoch oder spätestens nächstes Wochenende sollte ich dazukommen. Wenn dir das zu langsam geht, kannst du gerne auch selber daran arbeiten.

Was ich beim Rumschauen schon gesehen habe:

  1. BPD::tanRequiredForRequest() ist so ähnlich wie ich mir BPD::vopRequiredForRequest() vorstellen würde. Damit kann die FinTs-Klasse dann rausfinden, ob HKVPP hinzuzufügen ist oder nicht.
  2. Weil FinTs selbst die Antwort interpretiert und über einen Setter ähnlich wie BaseAction::setTanRequest() den Zustand der Action ändern würde, müsste man dieses filter-Rückmeldungen-Dings nicht mehr anfassen, weil BaseAction::processResponse() gar nichts von diesen Vorgängen wissen müsste. Diese Methode wird erst aufgerufen, wenn die ganzen VoP-Sachen durch sind und das Endergebnis empfangen wurde.
  3. Zu BaseAction::needsTime(), was ich in BaseAction::needsPollingWait() umtaufen würde, würden wir FinTs::pollAction() hinzufügen.
  4. Zu BaseAction::needsConfirmation() würden wir BaseAction::getConfirmationRequest() und FinTs::confirmAction() hinzufügen.
  5. Überall wo heute needsTan() erklärt wird (Dokumentation diverser Methoden und so), insbesondere dort wo darauf hingewiesen ist, wann es passiert und was man dann tun soll, und dass in dem Fall die Action eben noch nicht gleich im "Endzustand" ist wo man das Ergebnis aus ihren Gettern abholen kann; ... überall dort müsste man schauen, ob man weitere Erklärungen für die beiden neuen Zustände einfügen muss.

@Philipp91
Copy link
Contributor

Und wenn ich es richtig interpretiere, ist der Code in der Rückmeldung enthalten

Das ist so. Soweit so normal. Die Frage ist, ob phpFinTS richtig damit umgehen kann. Was passiert denn bei dir als nächstes?

@witschko
Copy link

Und wenn ich es richtig interpretiere, ist der Code in der Rückmeldung enthalten

Das ist so. Soweit so normal. Die Frage ist, ob phpFinTS richtig damit umgehen kann. Was passiert denn bei dir als nächstes?

Ich laufe in die Exception rein: https://github.com/ampaze/phpFinTS/blob/60a62eea2f775a3bd5a0f3dc2d25205a0a90e7b1/lib/Fhp/Action/SendSEPATransferVoP.php#L134

@ampaze
Copy link
Contributor Author

ampaze commented Oct 14, 2025

Vielen Dank für den Test!

Ich gehe davon aus, dass er funktioniert, also dass composer test keine Fehler wirft.

Korrekt. Das mit der while Schleife war eher zur Demonstration wieder Ablauf ist.

@Philipp91
Copy link
Contributor

eher zur Demonstration wieder Ablauf ist.

Das können wir in Samples/ dann durchaus auch so machen.

@ampaze
Copy link
Contributor Author

ampaze commented Oct 14, 2025

eher zur Demonstration wieder Ablauf ist.

Das können wir in Samples/ dann durchaus auch so machen.

Da der Test sowieso geändert werden muss (weil er auf SendTransferVoP beruht, was es ja dann nicht mehr geben wird), betrachte es eher als Zusatzinformation für dich 😄

@ampaze
Copy link
Contributor Author

ampaze commented Oct 14, 2025

Und wenn ich es richtig interpretiere, ist der Code in der Rückmeldung enthalten

Das ist so. Soweit so normal. Die Frage ist, ob phpFinTS richtig damit umgehen kann. Was passiert denn bei dir als nächstes?

Ich laufe in die Exception rein: https://github.com/ampaze/phpFinTS/blob/60a62eea2f775a3bd5a0f3dc2d25205a0a90e7b1/lib/Fhp/Action/SendSEPATransferVoP.php#L134

Du könntest mal deine sonstigen Logs mit meinen aus dem SendTransferVoPTest vergleichen. Laufen die Tests normal durch?

@ampaze
Copy link
Contributor Author

ampaze commented Oct 14, 2025

Die Bank liefert einen pain.002.001.10 XML Customer Report

Kann diese Bibliothek dieses Format heute schon parsen? Oder wäre es ziemlich einfach, das zu implementieren? Oder ist es eher etwas, was man wie SEPA-XML an eine andere Bibliothek outsourcen würde?

FinTs kann das nicht parsen, das Format ist aber relativ einfaches XML. Zwei Beispiele sind in dem SendTransferVoPTest enthalten.

Kannnst/möchtest du mit diesem Test die Implementation machen?

Am Mittwoch oder spätestens nächstes Wochenende sollte ich dazukommen. Wenn dir das zu langsam geht, kannst du gerne auch selber daran arbeiten.

Sag doch bescheid ob du morgen daran arbeitest.

Was ich beim Rumschauen schon gesehen habe:
3. Zu BaseAction::needsTime(), was ich in BaseAction::needsPollingWait() umtaufen würde, würden wir FinTs::pollAction() hinzufügen.

Das heißt du hast vor den state bzw. HIVPP in der Action vorzuhalten? Bin irgendwie davon ausgegangen, dass FinTs sich darum kümmert. Den Prüfbericht abzufragen benötigt keine Informationen aus der eigentlichen Action.

  1. Zu BaseAction::needsConfirmation() würden wir BaseAction::getConfirmationRequest() und FinTs::confirmAction() hinzufügen.

Hier würde ich glaube ich explizit Vop in den Namen der Methoden mit reinnehmen, weil Confirmation auch für einen Teil der TAN Bestätigung gehalten werden könnte.

@Philipp91
Copy link
Contributor

Sag doch bescheid ob du morgen daran arbeitest.

Fange gerade an.

Das heißt du hast vor den state bzw. HIVPP in der Action vorzuhalten?

Ja genau, weil als "Zustand" gehört es zur Action. Es gibt auch Zustand, der zu FinTs gehört, z.B. die Dialog-ID, die aktuelle Nachrichtennummer im Dialog, die BPD und so weiter. Aber für VoP gehört aller Zustand in die Actions. Theoretisch könnte der Nutzer auch mit mehreren Actions gleichzeitig jonglieren, die an unterschiedlichen Stellen im VoP-Prozess festhängen -- wenn der Zustand dann in FinTs gespeichert würde, käme etwas durcheinander.

Das hält uns aber nicht davon ab, die APIs inbesondere für das Auslösen von Aktionen wie "VoP bestätigen" oder so in FinTs zu packen.

@Philipp91
Copy link
Contributor

Hier würde ich glaube ich explizit Vop in den Namen der Methoden mit reinnehmen, weil Confirmation auch für einen Teil der TAN Bestätigung gehalten werden könnte.

Dann sollte man es ja eigentlich payeeVerification nennen, damit es nicht doppelt gemoppelt ist. Aber ich glaube, VOP ist so ein festehender Begriff dass wir davon lieber nicht abweichen sollten. Also halt vopVerification.

@nemiah nemiah mentioned this pull request Oct 15, 2025
$response->findRueckmeldung(Rueckmeldungscode::VOP_KEINE_NAMENSABWEICHUNG) !== null
// The bank has discarded the request, and wants us to resend it with a HKVPA
// This can happen even if the name matches.
|| $response->findRueckmeldung(Rueckmeldungscode::FREIGABE_KANN_NICHT_ERTEILT_WERDEN) !== null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es gibt drei alternative Bedingungen in diesem if. Wenn wir hier in dieses if reingehen, wird vopNeedsConfirmation = true gesetzt. Das überrascht mich:

  1. Zur ersten Bedingung VOP_KEINE_NAMENSABWEICHUNG finde ich nicht viel in der Spezifikation, aber ich würde vermuten es bedeutet "ist auch ohne VOP okay", also wäre vopNeedsConfirmation = false angemessen.
  2. Bei der zweiten Bedingung FREIGABE_KANN_NICHT_ERTEILT_WERDEN habe ich das Gefühl, dass es ein Nebenschauplatz ist. Wenn die Bank VOP machen will, dann dauert das erst Mal und am Ende ist eventuell eine Bestätigung nötig. Darum verwirft sie den aktuellen TAN-Request (denn wenn eine TAN-Challenge enthalten wäre, hätte die eventuell ein Timeout z.B. aus kryptographischen Gründen, das während der VOP-Prüfung ablaufen könnte). Wenn VOP dann durch ist, sollen wir laut Spezifikation einen frischen Anlauf mit der TAN starten. Wir könnten zwar den Abbruch des TAN-Requests als Signal interpretieren, dass VOP stattfindet aber es kommt mir ziemlich implizit vor. Ich hoffe, dass die erste Antwort der Bank (SEND_TRANSFER_RESPONSE im Unit Test) noch hilfreichere Signale in die Richtung enthält.
    • Und selbst wenn wir erkennen, dass VOP stattfindet, sollten wir noch nicht vopNeedsConfirmation setzen, sondern nur vopIsPending. Denn bis jetzt arbeitet die Bank ja nur an der Prüfung und es kann durchaus sein, dass sie nach der Verarbeitung noch zu dem Schluss kommt, dass keine Bestätigung durch den Nutzer mehr nötig ist.
  3. VOP_ERGEBNIS_NAMENSABGLEICH_PRUEFEN Das ist genau die Nachricht, wo ich vopNeedsConfirmation = true setzen würde. Es heißt ja schon wörtlich fast gleich.

Also ist jetzt die Frage, wie man der SEND_TRANSFER_RESPONSE ansieht, dass (1) VOP stattfindet und (2) man noch warten/pollen muss. Wir haben diese Segmente darin:

  1. HIRMS:4:2:3+3040::Es liegen weitere Informationen vor.:staticscrollref' => PAGINATION aka Aufsetzpunkt.
    • Dieses Segment wird ein paar Zeilen weiter oben erkannt und paginationToken daraus befüllt (sonst nichts).
    • Ich habe den Eindruck, dass das das Signal ist, nach dem wir suchen. Klingt das plausibel? Die Idee ist, dass der Client immer gleich auf einen Aufsetzpunkt reagiert: Das betroffene Segment nochmal einreichen und die Antworten aneinanderhängen, so lange bis der Aufsetzpunkt verschwindet -- denn dann hat man das Ende ("die letzte Seite") erreicht.
  2. HIVPP:6:1:3+++@36@c0f5c2a4-ebb7-4e72-be44-c68742177a2b+++++2' Hier ist das pollingId-Feld belegt. Wenn das so ist, müssen wir sie beim Polling zurückliefern. Aber wenn sie nicht belegt wäre, würde Polling trotzdem stattfinden, einfach nur mit dem Aufsetzpunkt allein.

Ich gehe mal davon aus, dass meine Vermutung richtig ist, und implementiere es in FinTs allein aufgrund vom Aufsetzpunkt. Hoffentlich funktioniert es dann trotzdem mit deinem Unit-Test zusammen, ohne dass dieser geändert werden muss.

Copy link
Contributor Author

@ampaze ampaze Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Habe auch wenig zu VOP_KEINE_NAMENSABWEICHUNG gefunden, aber Bank (bzw. Atruvia) verlangt soweit ich es ausprobiert habe auch bei Namensgleichheit, dass man die Ausführungsauftrag sendet. Ich denke die haben versucht den Prozess einheitlich zu gestalten, sodass der Ablauf immer gleich ist, egal wie das Ergebnis der Prüfung ist.

  2. FREIGABE_KANN_NICHT_ERTEILT_WERDEN wird zumindest von Atruvia auch bei Namensgleichheit geschickt, das hab ich gerade noch mal in den Logs nachgeguckt. Es ist also ein Ausführungsauftrag erforderlich.


In der Spezifikation steht explizit dass alleine die Rückmeldungscode ausschlaggebend sind, nicht die sonstigen Segmente. So hab ich das auch implementiert. Zitat:

Der Ablauf wird grundsätzlich nur durch die Rückmeldungscodes gesteuert und nicht durch das Prüfergebnis im HIVPP.
Es kann also vorkommen, dass der Rückmeldungscode 3945 "Freigabe kann nicht erteilt werden" auch bei einem VOP-Prüfergebnis RCVC (Match) gesendet wird. Dies gilt insbesondere für das Polling oder aber bei Opt-Out mit Decoupled-Verfahren. Das Kundenprodukt hat dann den Ablauf analog zum Close-/No-Match/Not Applicable-Ablauf (d.h. Neueinreichung des ZV-Auftrags und HKTAN in Verbindung mit HKVPA) fortzusetzen (s. Punkt 4. und Ablaufdiagramme_E.8.1.1.2 bzw._E.8.1.2.2 und_E.8.1.2.4..
Implementierungsbedingt kann es vorkommen, dass auch im Match-Fall einen Rückmeldungscode 3090 gesendet wird.
Implementierungsbedingt kann es vorkommen, dass ein Institut auch bei einem Match-Ergebnis immer mit einem Rückmeldungscode 3945 „Freigabe kann nicht erteilt werden" antwortet und grundsätzlich eine erneute Einreichung des ZV-Auftrags und HKTAN in Verbindung mit HKVPA erwartet.

@Philipp91
Copy link
Contributor

Hast du mal ausprobiert / weißt du, ob man nach SEND_TRANSFER_RESPONSE und vor der darauffolgenden POLL_VOP_REPORT_REQUEST die Verbindung unterbrechen kann (und evtl. sogar den Dialog beenden kann) und dann in einer frischen Session mit dem alten Aufsetzpunkt und/oder der alten Polling-ID weitermachen kann?

(Die Spezifikation erwähnt mehrere Dialoge nur im Rahmen der Mehrfachsignatur.)

@ampaze
Copy link
Contributor Author

ampaze commented Oct 15, 2025

Hast du mal ausprobiert / weißt du, ob man nach SEND_TRANSFER_RESPONSE und vor der darauffolgenden POLL_VOP_REPORT_REQUEST die Verbindung unterbrechen kann (und evtl. sogar den Dialog beenden kann) und dann in einer frischen Session mit dem alten Aufsetzpunkt und/oder der alten Polling-ID weitermachen kann?

(Die Spezifikation erwähnt mehrere Dialoge nur im Rahmen der Mehrfachsignatur.)

Nein hab ich noch nicht, kann ich mit deiner Implementierung dann gerne testen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants